package ecc;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Random;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @author  Paulo S. L. M. Barreto
 */
public class ETests {

    /**
     * Generic prototype variable used in the EPoint tests.
     */
    EPoint prototype;

    /**
     * Create an instance of ETests by providing prototypes
     * for EPoint and GF variables.
     *
     * This is a direct application of the "Prototype" design pattern
     * as described by E. Gamma, R. Helm, R. Johnson and J. Vlissides in
     * "Design Patterns - Elements of Reusable Object-Oriented Software",
     * Addison-Wesley (1995), pp. 117-126.
     *
     * @param   prototype   the prototype for EPoint instantiation
     */
    public ETests(EPoint prototype) {
        this.prototype = prototype;
    }

    /**
     * Perform a complete test suite on the ECurve implementation
     *
     * @param   iterations  the desired number of iterations of the test suite
     * @param   random      the source of randomness for the various tests
     */
    public void doTest(int iterations, SecureRandom rand) {
        EPoint w, x, y, z, ecZero;
        BigInteger m, n;
        int numBits = 256; // caveat: maybe using larger values is better
        long totalElapsed = -System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            System.out.print("test #" + i);
            long elapsed = -System.currentTimeMillis();
            // create random values from the prototype:
            x = prototype.randomize(rand);
            y = prototype.randomize(rand);
            z = prototype.randomize(rand);
            ecZero = prototype.E.infinity;
            m = new BigInteger(numBits, rand);
            n = new BigInteger(numBits, rand);

            // check cloning/comparison/pertinence:
            if (iterations == 1) {
                System.out.print("\nchecking cloning/comparison/pertinence");
            }
            if (!x.equals(x)) {
                throw new RuntimeException("Comparison failure");
            }
            System.out.print(".");
            if (!x.isOnSameCurve(x)) {
                throw new RuntimeException("Inconsistent pertinence self-comparison");
            }
            System.out.print(".");
            if (!x.E.contains(x)) {
                throw new RuntimeException("Inconsistent curve pertinence");
            }
            System.out.print(".");

            // check addition properties:
            if (iterations == 1) {
                System.out.print(" done.\nchecking addition properties");
            }
            if (!x.add(y).equals(y.add(x))) {
                throw new RuntimeException("x + y != y + x");
            }
            System.out.print(".");
            if (!x.add(ecZero).equals(x)) {
                throw new RuntimeException("x + 0 != x");
            }
            System.out.print(".");
            if (!x.add(x.negate()).isZero()) {
                throw new RuntimeException("x + (-x) != 0");
            }
            System.out.print(".");
            if (!x.add(y).add(z).equals(x.add(y.add(z)))) {
                throw new RuntimeException("(x + y) + z != x + (y + z)");
            }
            System.out.print(".");
            if (!x.negate().negate().equals(x)) {
                throw new RuntimeException("-(-x) != x");
            }

            // check scalar multiplication properties:
            if (iterations == 1) {
                System.out.print(" done.\nchecking scalar multiplication properties");
            }
            if (!x.multiply(BigInteger.valueOf(0L)).equals(ecZero)) {
                throw new RuntimeException("0*x != 0");
            }
            System.out.print(".");
            if (!x.multiply(BigInteger.valueOf(1L)).equals(x)) {
                throw new RuntimeException("1*x != x");
            }
            System.out.print(".");
            if (!x.multiply(BigInteger.valueOf(2L)).equals(x.twice(1))) {
                throw new RuntimeException("2*x != twice x");
            }
            System.out.print(".");
            if (!x.multiply(BigInteger.valueOf(2L)).equals(x.add(x))) {
                throw new RuntimeException("2*x != x + x");
            }
            System.out.print(".");
            if (!x.multiply(BigInteger.valueOf(-1L)).equals(x.negate())) {
                throw new RuntimeException("(-1)*x != -x");
            }
            System.out.print(".");
            if (!x.multiply(m.negate()).equals(x.negate().multiply(m))) {
                throw new RuntimeException("(-m)*x != m*(-x)");
            }
            System.out.print(".");
            if (!x.multiply(m.negate()).equals(x.multiply(m).negate())) {
                throw new RuntimeException("(-m)*x != -(m*x)");
            }
            System.out.print(".");
            if (!x.multiply(m.add(n)).equals(x.multiply(m).add(x.multiply(n)))) {
                throw new RuntimeException("(m + n)*x != m*x + n*x");
            }
            System.out.print(".");
            w = x.multiply(n).multiply(m);
            if (!w.equals(x.multiply(m).multiply(n))) {
                throw new RuntimeException("m*(n*x) != n*(m*x)");
            }
            System.out.print(".");
            if (!w.equals(x.multiply(m.multiply(n)))) {
                throw new RuntimeException("m*(n*x) != (m*n)*x");
            }
            // TODO: test point compression/expansion/conversion
            elapsed += System.currentTimeMillis();
            System.out.println(" done; elapsed =  " + (float)elapsed/1000 + " s.");
        }
        totalElapsed += System.currentTimeMillis();
        System.out.println("All " + iterations + " tests done in " + (float)totalElapsed/1000 + " s.");
    }

    protected static String p1024 =
        "8c7aeda4402c765a17bd27640d8ad98fe30274b28973f47cc0f99e6a819e16b4"+
        "864d3db5522a8b6da795cc78a659160e4c03f16f00be1ae56c13a5d63e5bfcc6"+
        "ce9978f72c6cd33ecbfe29f6780cf33f5098ff4ca9fcc379b0bdec937fb8833a"+
        "aa27b3b297f4679fbaf5dd4477cbb6813226e93898a6d11acd9cebe0447ac793";
    protected static String p2048 =
        "e51317a61b0486ea295ea52a60a8d2744c1b333cbe37fbc029cbc0ab2f849223"+
        "93b0bd360eda144b3ce9fe61f6944b67c3898e586cbb25220818a906c52cbd67"+
        "c8e167973c72900fbe036458bf743a3724bb2faeda6d2a55ec3fb4119a5d3620"+
        "550591b6d6ff6e928f5a474a06c3b454695eb6f480bbd2c52ad47a0b45d37d4f"+
        "3e8c1e7a0755c126811a94ef60f3d896457fbd4228fd49af70f9874c6a14aeff"+
        "4e0958c516ef4343c39c5f3dad569db9c5847f1672f9728d5021545793be6254"+
        "5d9cf085c1bd59e38be70f0fe5edacb6f36cac52798e7cf3ad36b45cd75e8568"+
        "a120d0b75d0a12d9ef4d3f49426d556a4a776109c9c9d1fec07ebd0e6d1e915b";
    protected static String p3072 =
        "a42e470355f05406a23db7288482d143416ad0043f165e0dc2a43da02f502a4c"+
        "e1a7ffd8208b0692805a38014d2b1836ed83bbf53c3ff6b10a9f0d661c682784"+
        "68de16c5a7c63c3da3ff9d115f3c2a441582ec09c282416dc4627ba3ba2aad36"+
        "231192ed2dadb67b682ab42f479c2aa6ac2229b3f124069bf38b03dd942b4c79"+
        "308fa7c004ed9193a6287057b8e660aa891d10a18b94420c8a1e5b1b9715b587"+
        "0a086b08f108cecdf255252e34bec511483f7f2061a3701d909831be6ae75019"+
        "b3884ec85111d1d62885e854c47231d56dcda821dea1b1f20ff5a413cb8dd0c4"+
        "4dc56935fe82b958ea26145da5e81449f5ccd154b964713ebf1bcc310dfd1c36"+
        "9f8c24e67992eecfe462dd3e22e2aa114932c418b748d50cdd2171a1ee59ab3e"+
        "c4324035361edbca6b685d36c74464a87f2be50b0d6529c9f3f8ecfddb04d3bf"+
        "137a8d320915c777fe761ed07603bd3b49f96eb5e87dc4566e131fff525b4eb3"+
        "cd1c415a964ec668dfeffb7631b1d9c6333e6523c610ec166e7b3987158dc8f5";

    public long doDLTiming(int iterations, SecureRandom rand, int size) {
        // BigInteger p = new BigInteger(3072, 10, rand);
        // System.out.println("p = " + p.toString(16));
        BigInteger p, g, h;
        switch (size) {
        case 160:
            p = new BigInteger(p1024, 16);
            //g = new BigInteger(21024, rand).mod(p);
            g = BigInteger.valueOf(2L);
            break;
        case 224:
            p = new BigInteger(p2048, 16);
            //g = new BigInteger(2048, rand).mod(p);
            g = BigInteger.valueOf(2L);
            break;
        case 256:
            p = new BigInteger(p3072, 16);
            //g = new BigInteger(3072, rand).mod(p);
            g = BigInteger.valueOf(2L);
            break;
        default:
            throw new RuntimeException("size not supported");
        }
        long elapsed = -System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            h = g.modPow(new BigInteger(size, rand), p);
        }
        elapsed += System.currentTimeMillis();
        return elapsed;
    }

    public long doECTiming(int iterations, SecureRandom rand) {
        EPoint g = prototype.randomize(rand), h;
        int size = g.p.bitLength();
        long elapsed = -System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            h = g.multiply(new BigInteger(size, rand));
        }
        elapsed += System.currentTimeMillis();
        return elapsed;
    }

    public long doKGTiming(int iterations, SecureRandom rand) {
        ECurve E = prototype.E;
        EPoint g;
        int size = E.p.bitLength();
        long elapsed = -System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            g = E.kG(new BigInteger(size, rand));
        }
        elapsed += System.currentTimeMillis();
        return elapsed;
    }

    public static void timings(SecureRandom rand) {
        ECurve param;
        ETests t;
        long elapsed, elapsedDL;

        //*
        System.out.println("Timing SEC2 curve for 160-bit prime field");
        param = ECurve.getStandardCurve(160);
        t = new ETests(param.infinity);
        // elapsed = t.doECTiming(100, rand);
        elapsed = t.doKGTiming(100, rand);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsed/1000 + " s.");
        System.out.println();

        System.out.println("Timing GF(p) exponentiation for 160-bit exponent and 1024-bit p");
        t = new ETests(null);
        elapsedDL = t.doDLTiming(100, rand, 160);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsedDL/1000 + " s.");
        System.out.println("DL/EC ratio = " + (float)elapsedDL/(float)elapsed);
        System.out.println();
        //*/

        System.out.println("Timing NIST curve for 224-bit prime field");
        param = ECurve.getStandardCurve(224);
        t = new ETests(param.infinity);
        // elapsed = t.doECTiming(100, rand);
        elapsed = t.doKGTiming(100, rand);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsed/1000 + " s.");
        System.out.println();

        System.out.println("Timing GF(p) exponentiation for 224-bit exponent and 2048-bit p");
        t = new ETests(null);
        elapsedDL = t.doDLTiming(100, rand, 224);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsedDL/1000 + " s.");
        System.out.println("DL/EC ratio = " + (float)elapsedDL/(float)elapsed);
        System.out.println();

        System.out.println("Timing NIST curve for 256-bit prime field");
        param = ECurve.getStandardCurve(256);
        t = new ETests(param.infinity);
        // elapsed = t.doECTiming(100, rand);
        elapsed = t.doKGTiming(100, rand);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsed/1000 + " s.");
        System.out.println();

        System.out.println("Timing GF(p) exponentiation for 256-bit exponent and 3072-bit p");
        t = new ETests(null);
        elapsedDL = t.doDLTiming(100, rand, 256);
        System.out.println("Elapsed time for 100 iterations: " + (float)elapsedDL/1000 + " s.");
        System.out.println("DL/EC ratio = " + (float)elapsedDL/(float)elapsed);
        System.out.println();
    }

    public static void generalTests(int iterations, SecureRandom rand) {
        ECurve param;
        ETests t;

        System.out.println("Loading NIST curve for 192-bit prime field");
        param = ECurve.getStandardCurve(192);
        t = new ETests(param.infinity);
        t.doTest(iterations, rand);
        System.out.println();

        System.out.println("Loading NIST curve for 224-bit prime field");
        param = ECurve.getStandardCurve(224);
        t = new ETests(param.infinity);
        t.doTest(iterations, rand);
        System.out.println();

        System.out.println("Loading NIST curve for 256-bit prime field");
        param = ECurve.getStandardCurve(256);
        t = new ETests(param.infinity);
        t.doTest(iterations, rand);
        System.out.println();

        System.out.println("Loading NIST curve for 384-bit prime field");
        param = ECurve.getStandardCurve(384);
        t = new ETests(param.infinity);
        t.doTest(iterations, rand);
        System.out.println();

        System.out.println("Loading NIST curve for 521-bit prime field");
        param = ECurve.getStandardCurve(521);
        t = new ETests(param.infinity);
        t.doTest(iterations, rand);
        System.out.println();
    }

    public static void svTest(int bits, SecureRandom rand) throws NoSuchAlgorithmException {
        System.out.print("Preparing for " + bits + "-bit signing/verification tests...");
        ECurve E = ECurve.getStandardCurve(bits); // load curve equation for E
        Object[] pair = ESigns.makeKeyPair(E, rand);
        if (pair == null) {
            System.out.println(" unable to make key pair!");
            return;
        }
        BigInteger x = (BigInteger)pair[0];
        EPoint Y = (EPoint)pair[1];
        ESigns sv = new ESigns();
        byte[] msg = new byte[100];
        rand.nextBytes(msg);
        System.out.println(" done.");
        byte[] r, sig;

        //*
        System.out.println("Testing " + bits + "-bit ECDSA signing/verification");
        long elapsed = -System.currentTimeMillis();
        int tests = 100;
        for (int i = 0; i < tests; i++) {
            r = sv.init(ESigns.ECDSA, E, MessageDigest.getInstance("SHA"), rand);
            //System.out.print("i");
            //sv.update(msg, 0, i); // sign the first i characters of msg
	    sv.update(msg, 0, msg.length); // signs the whole msg
            //System.out.print("u");
            sig = sv.sign(x);
            //System.out.print("s");

            r = sv.init(ESigns.ECDSA, E, MessageDigest.getInstance("SHA"), Y, sig, 0);
            //System.out.print("i");
            //sv.update(msg, 0, i); // verify the first i characters of msg
	    sv.update(msg, 0, msg.length); // verify the whole msg
            //System.out.print("u");
            boolean ok = sv.verify();
            if (!ok) {
                System.out.println(" verification error!");
                break;
            }
            //System.out.print("v");
        }
        elapsed += System.currentTimeMillis();
        System.out.println(" done in " + 1.0*elapsed/tests + " ms/sig.");
        //*/

        /*
        System.out.println("Testing " + bits + "-bit ECNR signing/verification");
        for (int i = 0; i < msg.length; i++) {
            r = sv.init(ESigns.ECNR, E, MessageDigest.getInstance("SHA"), rand);
            //System.out.print(".");
            sv.update(msg, 0, i); // sign the first i characters of msg
            //System.out.print(".");
            sig = sv.sign(x);
            //System.out.print(".");

            r = sv.init(ESigns.ECNR, E, MessageDigest.getInstance("SHA"), Y, sig, 0);
            //System.out.print(".");
            sv.update(msg, 0, i); // verify the first i characters of msg
            //System.out.print(".");
            boolean ok = sv.verify();
            if (!ok) {
                System.out.println(" verification error!");
                break;
            }
            System.out.print("+");
        }
        System.out.println(" done.");
        //*/
    }

    public static void aimTest(int bits, SecureRandom rand, int tests) throws NoSuchAlgorithmException {
        int errors = 0;
        // setup parameters:
        System.out.print("Preparing for " + bits + "-bit AIM signing/verification tests...");
        ECurve E = ECurve.getStandardCurve(bits); // load curve equation for E
        Object[] pair = ESigns.makeKeyPair(E, rand);
        if (pair == null) {
            System.out.println(" unable to make key pair!");
            return;
        }
        BigInteger x = (BigInteger)pair[0];
        EPoint Y = (EPoint)pair[1];
        ESigns sv = new ESigns();
        System.out.println(" done.");

        System.out.println("Testing " + bits + "-bit AIM-ECDSA signing/verification...");
        MessageDigest hash = MessageDigest.getInstance("SHA"); // or whatever hash function is chosen
        for (int i = 0; i < tests; i++) {
            // select message for signing/verification:
            byte[] msg = new byte[3000]; // or whatever the message is...
            rand.nextBytes(msg);

            // initialize signing:
            byte[] r = sv.init(ESigns.ECDSA, E, MessageDigest.getInstance("SHA"), rand);

            // compute message digest:
            hash.reset(); // N.B. it is possible to use here a MAC initialized with r.
            hash.update(msg);
            byte[] h = hash.digest();

            // compute signature:
            byte[] sig = sv.sign(x, h);

            // initialize verification:
            r = sv.init(ESigns.ECDSA, E, MessageDigest.getInstance("SHA"), Y, sig, 0);

            // compute message digest:
            hash.reset(); // N.B. it is possible to use here a MAC initialized with r.
            hash.update(msg);
            byte[] d = hash.digest();

            // verify signature:
            boolean ok = sv.verify(d);
            if (!ok) {
                errors++;
                System.out.print("-");
            } else {
                System.out.print("+");
            }
        }
        System.out.println(" done.");
        System.out.println("# errors found: " + errors);
    }

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
        boolean general = false;
        byte[] randSeed = new byte[20];
        (new Random()).nextBytes(randSeed);
        /*
        for (int i = 0; i < randSeed.length; i++) {
            randSeed[i] = (byte)i;
        }
        //*/
        SecureRandom rand = new SecureRandom(randSeed);
        if (general) {
            //int iterations = (args.length > 0) ? Integer.parseInt(args[0]) : 1;
            generalTests(10, rand);
            timings(rand);
        } else {
            svTest(256, rand);
            //aimTest(224, rand, 100);
        }
    }
}
